feat(webhooks): verifyAndParse* API for compressed payloads (CHA-3071)#255
Open
nijeesh-stream wants to merge 5 commits intomainfrom
Open
feat(webhooks): verifyAndParse* API for compressed payloads (CHA-3071)#255nijeesh-stream wants to merge 5 commits intomainfrom
nijeesh-stream wants to merge 5 commits intomainfrom
Conversation
Adds App.decompressWebhookBody and App.verifyAndDecodeWebhook so handlers can accept the new outbound webhook compression (GetStream/chat#13222) without changing how X-Signature is verified. decompressWebhookBody returns the body unchanged when Content-Encoding is null or empty, gunzips with java.util.zip.GZIPInputStream when the header is gzip (case-insensitive, trimmed), and throws IllegalStateException for any other value with a message that points the operator at the app's webhook_compression_algorithm setting. verifyWebhookSignature gains a byte[] overload so the existing String overload no longer round-trips through UTF-8 unnecessarily, and the equality check moves to MessageDigest.isEqual so comparison is constant-time. verifyAndDecodeWebhook chains decompression with the HMAC check and returns the raw JSON when the signature matches; SecurityException is thrown otherwise. The signature is always computed over the uncompressed bytes, matching the server. The webhook docs are updated with the new Content-Encoding header row and a worked example using verifyAndDecodeWebhook. Tests cover gzip round-trip, null/empty/whitespace passthrough, case- insensitive Content-Encoding, invalid gzip bytes, every non-gzip encoding rejected with a clear hint, byte[] / String HMAC overload parity, signature mismatch, and the regression case where the signature was computed over the compressed bytes. Co-authored-by: Cursor <cursoragent@cursor.com>
Extends `decompressWebhookBody` and `verifyAndDecodeWebhook` with an optional `payloadEncoding` argument. When set to "base64" (the wrapper Stream applies for SQS / SNS firehose so the message stays valid UTF-8 over the queue), the body is base64-decoded before gzip decompression. The HMAC signature continues to be computed over the innermost (uncompressed, base64-decoded) JSON, so the verification rule is invariant across HTTP webhooks and SQS / SNS. `null` / `""` for payloadEncoding is a no-op, so the HTTP webhook path is byte-identical to before this change. The existing 3-argument overloads of `decompressWebhookBody` and `verifyAndDecodeWebhook` are preserved for backward compatibility. Co-authored-by: Cursor <cursoragent@cursor.com>
Replaces verifyAndDecodeWebhook / decompressWebhookBody on App with the cross-SDK contract documented at https://getstream.io/chat/docs/node/webhooks_overview/. Static helpers on App: Primitives: ungzipPayload - gzip magic-byte detection + inflate decodeSqsPayload - base64 then ungzip-if-magic (String -> byte[]) decodeSnsPayload - alias for decodeSqsPayload verifySignature - constant-time HMAC-SHA256 comparison (parameter order matches the cross-SDK spec) parseEvent - JSON -> typed Event via Jackson Composite (return Event): verifyAndParseWebhook verifyAndParseSqs verifyAndParseSns Each composite has a singleton-secret overload that pulls the API secret from Client.getInstance(), so handler code stays terse. The composite functions auto-detect compression from body bytes, keeping the same handler correct whether or not Stream is currently compressing payloads, and behind middleware that auto-decompresses. Backward compatibility: * App.verifyWebhook(body, signature) -> bool kept unchanged. * App.verifyWebhookSignature(...) overloads kept; they now delegate to verifySignature internally. Co-authored-by: Cursor <cursoragent@cursor.com>
RFC 1952 defines the gzip magic number as the two-byte sequence 1F 8B; the third byte (CM) is informational and not part of the identifier. Trim the magic check from three bytes to two to match the spec and stay consistent with the reference implementations in the public docs. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds first-class support for gzip-compressed webhook payloads (HTTP webhooks, SQS, SNS) and exposes a stable
verifyAndParse*API that mirrors the cross-SDK contract published in Webhooks Overview.New public API (
io.getstream.chat.java.models.App)Primitives:
ungzipPayload(byte[])— gzip-magic-byte detection, no-op when not compresseddecodeSqsPayload(String)/decodeSnsPayload(String)— base64 decode then ungzip-if-magicverifySignature(byte[], String, String)— constant-time HMAC-SHA256 over the uncompressed bodyparseEvent(byte[])— JSON → typedEventvia JacksonComposites (return a typed
Event):verifyAndParseWebhook(byte[], String, String)verifyAndParseSqs(String, String, String)verifyAndParseSns(String, String, String)Each composite has an overload that resolves the secret from the configured singleton client.
Backwards compatibility
verifyWebhookandverifyWebhookSignatureare preserved and delegate to the newverifySignaturehelper. Existing callers continue to work for plain (uncompressed) bodies.Tests
WebhookCompressionTestcovers plain / gzip / base64 / base64+gzip payloads, signature mismatches, malformed bytes, and JSON parsing intoEvent. Linked Linear ticket: CHA-3071.Test plan
./gradlew test --tests io.getstream.chat.java.WebhookCompressionTest— all green (run viaamazoncorretto:11Docker image)./gradlew build— passes